RESTful APIs done right

Anyone who has ever set up a domain with microservices already knows: APIs for service-to-service communication are of crucial importance. Since each team has its own style and implements interfaces differently, the number of various approaches tends to explode sooner or later. Defining a guideline with rules and examples right at the beginning of the project helps to guarantee consistent APIs which are as self-explanatory as possible.

By Thomas Spiegl and Manfred Geiler

REST with JSON is the most widely used approach for new programming interfaces today. Countless books, lectures, blogs and other sources on the internet are addressing the topic. In spite of this, there seem to be wide differences in opinion within the developer community as to how web interfaces should look. The JSON API specification [1] specifies exactly how a RESTful API based on a common standard should be implemented.

But first, a few words about APIs. A developer should take the time to define an API well, since well-designed APIs automatically lead to better products. It’s a good idea to develop guidelines at the beginning of a project which capture the key requirements for an API. By using keywords in accordance with RFC 2119 [2], you can determine how important (or crucial) each individual requirement is. Topics for a guidelines catalog can for example include API naming, HTTP headers and operations, query parameters, or document structure.

Guidelines for API design can help to develop a common understanding. Basically, we can state that services which follow common standards are simpler to understand and easier to integrate. In addition, they can be implemented more efficiently (shared tools), are more stable against changes, and easier to maintain.

Style guide for JSON APIs

The JSON API specification [1] includes specific suggestions as to what a well-designed REST API can look like. In our opinion, the specification is a good starting point into the topic. For our examples, we will conceptually step into the world of sports clubs. Using our conceived IT solution, we would like to manage teams, managers, and players.

 

Fig 1: Data model

The diagram (Fig. 1) shows the entity Team and its relationships. A Team is trained by a Manager. Multiple Players are assigned to a Team. Managers and Players are each of the type Person.

 

Manager or Coach?

The terms Manager or Head Coach in English football (soccer) are both synonyms for the head trainer of a team. For the sake of readability, we will use the term Manager here.

 

The API: Who can we talk to?

For our sample application, the central domain entity Team provides entry into the API. Classic operations such as reading, writing, changing, deleting, or managing relationships are made possible through an API URL path. The path also serves as documentation for the API user and should therefore be clear and easy to interpret. Providing the entry point for the Team resource is the /teams path. The path name is entered in the plural form, since after all, we should be able to manage several teams at the same time.

Therefore, we specify in the API that an entity can be managed via the /{entity-type} resource path. Typically, we deal with a collection here (1 to n teams). The following applies as the naming convention: an entity type is (or ends with) a plural noun, contains lowercase letters only and individual words are separated by a minus.

 

  • correct: /teams, /team-memberships
  • incorrect: /Teams, /team, /teamMemberships, /team-search

 

Query data objects: Which teams do we have in fact?

Let’s start with a simple HTTP GET on the API path /teams:

 

GET /teams HTTP/1.1

Accept: application/vnd.api+json

 

In the HTTP Header Accept the value application/vnd.api+json specifies that a JSON document is expected in the response. Each HTTP request should set this header. The returned document contains an array of all Team objects. A response thus might look something like in Listing 1.

Listing 1: Array of all “Team” objects

{
  "data": [
    {
      "id": "1",
      "type": "teams",
      "attributes": {
        "name": "FC Norden Jugend",
        "category": "juniors"
      },
      "links": {
        "self": "http://example.com/teams/1"
      }
    },
    {
      "id": "2",
      "type": "teams",
      "attributes": {
        "name": "FC Essen",
        "category:" "masters"
      },
      "links": {
        "self": "http://example.com/teams/2"
      }
    }
  ]
}

The document structure: Order is half the battle

A well-structured API also includes a specified document structure. It helps build reusable tools and also facilitate the interaction of components and data in the user interface. An API with a common structure works like it’s been made from the same mold – even if different development teams are working on its definition. The structure of a JSON document is precisely defined by the JSON API Specification [3]. Each document always includes one of two elements: data or errors. Optionally, the meta, jsonapi, links or included elements can be included on the highest level. The data element contains the actual data and can be made up of either a single resource object or an array of resource objects. In addition, object references or even null can be included. Looking at the document from the last example, we see that in the data element, a resource array and a list of data objects of the type teams are delivered. In turn, the data object has the elements id, type, attributes and links. The attributes id and type represent a reference {“id”: “1”, “type”: “teams”} to an entity of the type teams. Each resource must have these two attributes. This way, data objects detached from the API can still be clearly identified. The type corresponds to the path name in the API, so it’s always a noun in the plural form. The actual data (so for example “name”: “FC Essen”) of the entity is listed in the attributes element. It’s not very typical to keep data separate from the object reference. In classic RDBMS or in JPA, data and identifiers are usually listed in the entity on equal terms. Still, separation is useful, since type and id are purely technical distinctions of the object and should not be mixed with the technical attributes. If we omit all other attributes, we always additionally receive the object reference {“id”: “1”, “type”: “teams”}.

Searching for data objects: Where is my team?

To search for our teams, we will append URL query parameters to the URL path as usual. To filter by attribute values, the specification provides parameters with the naming pattern filter[{attribute_name}]. Distinction in the attributes takes place through the associative parameter {attribute_name}. The search for the team “FC Norden” would then for example look like this:

 

GET /teams?filter[name]=FC+Norden

 

Filter parameters can be linked using a logical AND:

 

GET /teams?filter[name]=FC+Norden+Jugend&filter[category]=juniors

 

or they can contain a set of values, which corresponds to a logical OR:

 

GET /teams?filter[category]=juniors,masters

 

The reserved name filter has a great advantage – it immediately becomes clear to the user of the API what the URL parameter is used for.

Scrolling through the result: back and forth …

The principle of associative parameters also proves to be an elegant solution when it comes to scrolling through a result list: Two URL parameters, page[number] and page[size], are sufficient. Even without documentation, the meaning of the parameters is immediately obvious to the user. The following query lists teams on page three with a maximum of ten results per page (Listing 2).

Listing 2: Scrolling through the result list

GET /teams?page[number]=3&page[size]=10 HTTP/1.1
Accept: application/vnd.api+json

{
  "data": [
  { /*...*/ },
  { /*...*/ },
  /*...*/
  ],
  "links": {
    "first": "http://example.com/teams?[number]=1&page[size]=10",
    "prev": "http://example.com/teams?page[number]=2&page[size]=10",
    "next": "http://example.com/teams?page[number]=4&page[size]=10"
    "last": "http://example.com/teams?page[number]=200&page[size]=10"
  }
}

The links for scrolling are provided in the response document. The specification for this can be found in the links element parallel to data. Thus it also becomes clear why it is advantageous to deliver actual data in its own data element. On the highest level, other important data can be additionally transmitted to the receiver of the result. This characteristic of the JSON API document is also useful in handling errors.

Create a new data object: Welcome FC Oldenburg

So how do we create a new team? The API path /teams is used here as well, though we nevertheless still use the HTTP method POST. The request now looks like what is presented in Listing 3.

 

Listing 3

POST /teams HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": {
    "type": "teams",
    "attributes": {
      "name": "FC Oldenburg",
      "category": "seniors"
    }   
  }
}

We again use the document attribute data to transfer the actual team attribute. The id is assigned by the server in this example and is therefore not specified in the document. The question arises as to why the “type”:”teams” information cannot also be omitted: Couldn’t the type be derived from the API path /teams anyway? This is only partially the case. If the specification in the path happens to be an abstract base type that knows different polymorphic subclasses, we need a specific (not abstract) data type to generate it. So to be more robust against later expansions in the data model, we thus define the field type as mandatory from the outset. As a response to the POST request, the created team is delivered as a document (Listing 4).

 

Listing 4

{
  "data": {
    "type": "teams",
    "id": "3",
    "attributes": {
      "name": "FC Oldenburg",
      "category": "seniors"
    }   
    "links": {
      "self": "http://example.com/teams/3"
    }
  }
}

The id is assigned by the server and is now also included in the document. A link to the resource itself in returned in the self field.

 

HTTP Method POST

It is important to know that a POST changes the status in a domain by definition and is therefore not idempotent. Each POST leads to a new result. The request POST /teams HTTP/1.1 will therefore generate a new team each time. If a data object needs to be changed, POST must never be used; rather PATCH or PUT should be applied instead.

 

Read a data object: Show me the team with Number 3

To read a single data object, an API path is used which corresponds to the /{entity-type}/{entity-id} pattern. The path is thus extended by the id of the entity. Here we are referring to the unique URI of the associated resource. The self link in the previous example corresponds specifically to such a URI. The team with Number 3 will thus be read using a simple HTTP GET:

 

GET /teams/3 HTTP/1.1

Accept: application/vnd.api+json

 

As a response, the document with the reference {“type”:”teams”, “id”:”3″} will be delivered.

Change a data object: FC Essen has matured…

To change a team, we will again use the URI of the resource according to the pattern /{entity-type}/{entity-id}. Using the PATCH method, we can change individual attributes without overwriting the entire object (Listing 5).

 

Listing 5

PATCH /teams/2 HTTP/1.1
Accept: application/vnd.api+json
{
  "data": {
    "id": "2"
    "type": "teams",
    "attributes": {
      "category": "seniors"
    }
  }
}

We only change the category, attribute, while the name attribute remains unchanged. The entire (modified) document is returned as a response.

An HTTP PATCH query is idempotent. Each call with the same data will result in the same status in the domain. A repeated identical call will not change any data on the server or in our database.

Delete a data object: FC Oldenburg isn’t there anymore

To delete a team, we will use the URI of the resource according to the pattern /{entity-type}/{entity-id} just like for reading and changing. We simply use DELETE as the HTTP method now:

 

DELETE /teams/3 HTTP/1.1

 

As a response, we obtain a corresponding HTTP status code, so 200 OK, if the deletion was successful or 404 Not Found, if the object to be deleted does not exist.

About relationships: Who belongs to whom?

The data model for our example assumes that one team can have one manager and multiple players assigned. The Manager as well as the Players are of the type persons. The term Relationship is introduced to manage the Manager and Players of a team through the API. The following example shows how the relationship manager is depicted in the relationships element (Listing 6).

 

Listing 6: “Manager”-”Relationships” relationship

GET /teams/3 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
  "data": {
    "id:": "3"
    "type": "teams",
    "attributes": {
      "name": "FC Oldenburg"
    },
    "relationships": {
      "manager": {
        "data": { "type": "persons", "id": "55" },
        "links": {
          "self": "http://example.com/teams/3/relationships/manager",
          "related": "http://example.com/teams/3/manager"
        }
      }
    }
  }
}

Relationships are delivered in the data object in the relationships element. Each relationship is identified from the perspective of the referring entity by a unique name and contains the elements data and links. The data element can contain one or more data objects, depending on the cardinality of the relationship. In our specific example, the manager has a 1:1 relationship, so exactly one reference to the person data object of the manager is delivered. The links element contains two references, and the relationship can be self-managed using the self link. The pattern for the API path here is as follows: /{entity-type}/{entity-id}/relationship/{relationship-name}.

It is important to acknowledge here that the relationship link only addresses the relationship. So the link http://example.com/teams/3/relationships/manager delivers the relationship with the manager name, but not the document of the person referenced. This in turn is obtained using the second link with the name related. The link http://example.com/teams/3/manager delivers the document with the data on the person. Here as well you can see that the type in the document cannot always be derived from the request URL.

The 1:1 relationship: Our team needs a coach!

Now the manager with number 55 needs to be set for the team with number 3. The HTTP method PATCH is used to write a relationship:

 

PATCH /teams/3/relationships/manager HTTP/1.1

Content-Type: application/vnd.api+json

Accept: application/vnd.api+json

{

 “data”: { “type”: “persons”, “id”: “55” }

}

 

A relationship can also be deleted, as long as it is optional. The data element is set to null for this purpose:

 

PATCH /teams/3/relationships/manager HTTP/1.1

Content-Type: application/vnd.api+json

Accept: application/vnd.api+json

{

 “data”: null

}

The 1:n relationship: Who’s playing with us?

The approach is quite similar when managing player relationships. Unlike the team manager though, we have a 1:n relationship between the team and the players here. We have a choice between HTTP PATCH and POST (Listing 7).

 

Listing 7

PATCH /teams/3/relationships/players HTTP/1.1
{
  "data": [
    { "type": "persons", "id": "10" },
    { "type": "persons", "id": "11" },
    { "type": "persons", "id": "12" },
    { "type": "persons", "id": "13" }
  ]
}

POST /teams/3/relationships/players HTTP/1.1
{
  "data": [
    { "type": "persons", "id": "17" },
    { "type": "persons", "id": "18" }
  ]
}

The difference is not completely apparent at first, but it quickly becomes clear: PATCH replaces the complete relationship, so all the players on our team. POST on the other hand adds the specified persons to existing player relationships, as long as they do not already exist. Similarly, one or more player relationships can be deleted again using HTTP DELETE.

 

DELETE /teams/3/relationships/players HTTP/1.1

{

 “data”: { “type”: “persons”, “id”: “10” }

}

 

With DELETE the data element can also contain a single reference or an array, a list of references.

Integrate related objects: I want to see everything!

Using the possibilities shown so far, it would prove to be quite cumbersome to load all the data for a specific team, meaning the entire team including the names of the coach and players. You would first have to read the team object. As a result, the developer would have to issue another API call for each relation to load the associated person objects. That would not be very performant nor would it be much fun to program. JSON API specifies an elegant possibility here to load all the required relationships with a single API call. The URL parameter include is reserved for this purpose. If the person in the manager relationship should also need to be loaded, you would set this as the URL parameter: ?include=manager (Listing 8).

 

Listing 8: Load relationship objects

GET /teams/3?include=manager HTTP/1.1
Accept: application/vnd.api+json

{
  "data": {
    "id:": "3"
    "type": "teams",
    "attributes": {
      "name": "FC Oldenburg"
    },
    "relationships": {
      "manager": {
        "data": { "type": "persons", "id": "55" },
        "links": {
          "self": "http://example.com/teams/3/relationships/manager",
          "related": "http://example.com/teams/3/manager"
        }
      }
    }
  },
  "included": [
    {
      "id:": "55"
      "type": "persons",
      "attributes": {
        "name": "Coach Maier"
      }
    }
  ]
}

The data of the coach will now be delivered in the included element in parallel to the data element. Similarly, the players can also be requested using the very same call (Listing 9).

 

Listing 9: Request player

GET /teams/3?include=manager,players HTTP/1.1
Accept: application/vnd.api+json

{
  "data": {
    "id:": "3", "type": "teams",
    "attributes": { name": "FC Oldenburg" },
    "relationships": {
      "manager": {
        "data": { "type": "persons", "id": "55" },
        "links": { "related": "http://example.com/teams/3/manager" }
      },
      "players": {
        "data": [
          { "type": "persons", "id": "10" },
          { "type": "persons", "id": "11" }
        ],
        "links": { "related": "http://example.com/teams/3/players" }
      }
    }
  },
  "included": [
    {
      "id:": "55", "type": "persons",
      "attributes": { name": "Coach Maier" }
    },
    {
      "id:": "10", "type": "persons",
      "attributes": { name": "Johnny Wirbelwind" }
    },
    {
      "id:": "11", "type": "persons",
      "attributes": { name": "Franz Luftikus" }
    }
  ]
}

The include parameter is also ideal for searching, that is, when querying a list of data objects:

 

GET /teams?include=manager

 

The include method and the associated result structure have a few essential advantages over traditional approaches. The data of the individual objects remain strictly separated. An included object appears only once in the document, even when it is referenced multiple times. Finally, it avoids situations in which the API must be specifically tailored to the needs of different API consumers (for example. /teams/readAllWithManager?id=3 or similar outgrowths).

Here we should note that such a request on the server side can lead to very complex database queries. In our example, the following query: /teams?include=manager,players would deliver the entire team list including all coach and player data. But if you think a moment about the corresponding SQL database query using Joins, you can imagine that with large data volumes this may not exactly be the ideal solution. So it could make sense not to automatically allow the inclusion of certain relationships for all API URL paths. If we only want to read the players of a team in a query, all we have to do is let the self link from the players relationship element do its job again. The link from the example http://example.com/teams/3/players delivers as a response all objects of the type persons from the players relationship.

Perform actions: Do something!

So far, we have introduced classic actions such as Create, Read, Update or Delete. Yet if those basic operations are not sufficient, the API must be expanded accordingly. An action (such as ‘suspend player from team’) usually also introduces a verb into the name of the operation. On the other hand, the REST design pattern advises against the use of any verbs in the URL. How can additional server operations be integrated in the API? The JSON API specification does not elaborate on actions in the API. So we will present a few approaches based on an example: A player is injured and the team manager should be informed about this fact in a message. The corresponding method is already available on the server side and should now be included in the API.

Action Verb: Call me!

In the first variant, the action is included in the path: /players/1/notifyInjury. The use of the verb notify indicates a type of remote procedure call – something we really want to avoid in the RESTful approach. At any rate, notifyInjury is not a resource in the traditional sense. It is completely unclear as to which of the HTTP methods GET, POST or PATCH should be applied to this pseudo resource. Therefore, this method is not recommended.

Action Patch: React to my change!

If the verb does not appear in the API path, the server-side method can also be triggered by a simple PATCH (Listing 10).

 

Listing 10

PATCH /players/1 HTTP/1.1
{
  "data": {
    "id": "1"
    "type": "players",
    "attributes": {
      "condition": "injured"
    }
  }
}

Updates to the condition attribute are appropriately monitored on the server, and if the status changes, the team manager is notified about the injury of a player. The server-side action must be implemented idempotent for this variant – the manager may only be notified when there is a change in status, and not about each PATCH to this resource.

Action metadata: Send me a note!

The meta element (Listing 11), specified in the JSON API offers yet another option.

 

Listing 11

POST /players/1 HTTP/1.1
{
  "data": {
    "id": "1", "type": "players"
  },
  "meta": {
    "action": "notifyInjury"
  }
}

A POST to a resource is interpreted here as the sending of an action. Here, the action to be performed is given in the meta block as an action. It should be noted that a POST is not idempotent. Should the player already be injured, the server must respond with an error. The advantage of this variant is that no additional URL path is defined, but only a meaning is assigned to a POST to an existing resource.

Action Queue: Send me your action!

If you still want to carry out the action in the API path, you have the possibility of defining an action queue [4]. The /players/1/actions URL offers the possibility to create new actions (i.e. resources of the type actions) through the API as usual with the help of POST. Optionally, even past actions could be read out here using GET. The advantage of this variant is that an action is a (pseudo) resource and is in line with the REST pattern. The disadvantage: an additional path in the URL is needed.

Error handling: What’s going wrong here?

Status codes are used to inform the interface user about the output of the request processing. The HTTP protocol provides information about possible results (Table 1).

 

Status group Meaning
2xx Processing ok
3xx Alternate route
4xx Incorrect API operation
5xx Server-side error

Table 1: HTTP status codes

 

The JSON API specification offers the possibility to deliver error messages in the errors element, in addition to the HTTP status code (Listing 12).

 

Listing 12


HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
  "errors": [
    {
      "code": "100",
      "status": "400",
      "source": {"pointer": "data.attributes.name"},
      "title":  "Mandatory field",
      "detail": "Attribute ‘name‘ must not be empty"
    }
  ]
}

Here, an incorrect operation of the API by the client is indicated. The response does not contain a data element and instead, delivers one or more error messages with further details in addition to the HTTP Status 400 (bad request). The JSON API specification names additional attributes [5] for an error object. The use of HTTP status codes should not be exaggerated. At the HTTP level, the use of a few codes only is usually sufficient. In any case, the errors element of the document provides a detailed description.

Data types: What are you?

The JSON API specification hardly addresses the use of data types. In this regard, it only mentions that the definition of supported types for attribute values should absolutely be included in a guidelines catalog. Standard formats and the corresponding JSON presentation for data, time stamp or even custom classes like Money and the like should absolutely be determined in advance.

API versioning: Which dialect do you speak?

The first step is done quickly. An API has been developed and is in use. From this point on, the interface should be as stable and resistant to changes as possible. Additional attributes usually do not cause any distortion with the consumers of the interface. Clients should be robustly implemented against such customizations. But what if something fundamental should change in the schema of the API? If other development teams or even external partners are affected by the change, the synchronization of all necessary tasks can be costly and complicated. Therefore, the API developer also has to worry about the different versions of an API. The JSON specification does not offer any predefined solution for this issue. If you want to (or have to) be backward compatible and continue to support old calls, URI versioning can be a solution:

 

/v1/teams

/1.0/teams

/v1.1/teams

 

However, if you take the RESTful approach seriously, you need to be aware of the fact that with each introduction of a new version from an outsider viewpoint, a full new data model is created. The linking of resources between themselves should never be based on different API versions. To a client, it looks as if they would be independently existing data sources with different datasets.

Advantages in this regard include the simplicity of implementation (apart from backward compatibility) and uncomplicated use. A disadvantage is the fact that the same resource can be accessed under multiple paths. The URL for one and the same REST resource is not fixed for all time, and links in external systems may need to be customized.

In addition, a canonical identifier is missing: two different URIs can point to the same underlying resource without the client being aware of it. For additional approaches to versioning, we recommend the book “Build APIs You Won’t Hate” [6].

Conclusion: We are prepared

A strong trend in the software industry is moving away from monolith to small domain and microservices. This development requires stable and well thought-out APIs to prevent you from sinking into the interface chaos. The JSON API specification offers a number of good solution approaches within the scope of RESTful JSON. You can easily derive you own guidelines from it. If you’re well prepared for the API explosion from the outset, nothing will stand in the way of a successful solution.

Links & literature

  • [1] JSON API Specification: http://jsonapi.org
  • [2] Keywords for use in RFCs to Indicate Requirement Levels: https://www.ietf.org/rfc/rfc2119.txt
  • [3] JSON API/Document Structure: http://jsonapi.org/format/#document-structure
  • [4] Thoughts on RESTful API Design: http://restful-api-design.readthedocs.io/en/latest/methods.html#actions
  • [5] JSON API/Errors: http://jsonapi.org/format/#errors
  • [6] Sturgeon, Philip: „Build APIs You Won’t Hate“: https://leanpub.com/build-apis-you-wont-hate

Top Articles About Microservices & Service Mesh

STAY TUNED!

JOIN OUR NEWSLETTER

Behind the Tracks

Software Architecture & Design
Software innovation & more
Microservices
Architecture structure & more
Agile & Communication
Methodologies & more
DevOps & Continuous Delivery
Delivery Pipelines, Testing & more
Big Data & Machine Learning
Saving, processing & more

JOIN OUR UPCOMING EVENTS IN LONDON!